package ee.siimoliver.chat.server;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import ee.siimoliver.chat.GUI.ChatFrame;
import ee.siimoliver.chat.GUI.LoadingFrame;
import ee.siimoliver.chat.controllers.BusinessLogicController;
import ee.siimoliver.chat.controllers.ChatSessionController;
import ee.siimoliver.chat.utils.ApplicationProperties;
import ee.siimoliver.chat.utils.ChatUtils;
import ee.siimoliver.chat.utils.Const;
import ee.siimoliver.chat.utils.HttpConstants;
import ee.siimoliver.chat.utils.JsonParser;
import ee.siimoliver.chat.utils.RequestMethods;

public class WebServer implements HttpConstants, Const, Runnable, BusinessLogicController {

	protected ChatFrame chatFrame;
	private int port;
	private boolean isRunning;
	private ArrayList<ChatSessionController> chatSessions;
	private Object sessionLock = new Object();
	
	private ServerSocket ss;
	private Timer responseWaitTimer;
	private TimerTask addDummyMessage;
	
	/* static class data/methods */

    /* print to stdout */
    protected static void p(String s) {
        System.out.println(s);
    }
    /* print to the log file */
    protected void log(String s) {
        synchronized (log) {
            log.println(s);
            log.flush();
        }
    }
    
    public WebServer(ChatFrame chatFrame) {
    	responseWaitTimer = new Timer();
    	this.chatFrame = chatFrame;
    	this.chatFrame.attachBusinessLogicController(this);
    	chatSessions = new ArrayList<ChatSessionController>();
    }

    public WebServer() {}
    
    private PrintStream log = System.out;

    /* our server's configuration information is stored
     * in these properties
     */
    protected Properties props = new Properties();

    /* Where worker threads stand idle */
    Vector<Worker> threads = new Vector<Worker>();

    /* the web server's virtual root */
    File root;

    /* timeout on client connections */
    int timeout = 0;

    /* max # worker threads */
    int workers = 5;


    /* load www-server.properties from java.home */
    private void loadProps() {
        File f = new File
                (System.getProperty("java.home")+File.separator+
                    "lib"+File.separator+"www-server.properties");
        if (f.exists()) {
        	try {
	            InputStream is =new BufferedInputStream(new
	                           FileInputStream(f));
	            props.load(is);
	            is.close();
	            String r = props.getProperty("root");
	            if (r != null) {
	            	root = new File(r);
	            	if (!root.exists()) {
	            		throw new Error(root + " doesn't exist as server root");
	            	}
	            }
	            r = props.getProperty("timeout");
	            if (r != null) {
	            	timeout = Integer.parseInt(r);
	            }
	            r = props.getProperty("workers");
	            if (r != null) {
	            	workers = Integer.parseInt(r);
	            }
	            r = props.getProperty("log");
	            if (r != null) {
	            	p("opening log file: " + r);
	            	log = new PrintStream(new BufferedOutputStream(
	            			new FileOutputStream(r)));
	            }
        	} catch (Exception e) {
        		e.printStackTrace();
        	}
        }

        /* if no properties were specified, choose defaults */
        if (root == null) {
            root = new File(System.getProperty("user.dir"));
        }
        if (timeout <= 1000) {
            timeout = 5000;
        }
        if (workers < 25) {
            workers = 5;
        }
        if (log == null) {
            p("logging to stdout");
            log = System.out;
        }
    }

    private void printProps() {
        p("root="+root);
        p("timeout="+timeout);
        p("workers="+workers);
    }

    @Override
    public void run() {
    	isRunning = true;
        port = Integer.parseInt(ApplicationProperties.getInstance().getProperty(Const.PROP_PORT));
        loadProps();
        printProps();
        /* start worker threads */
        for (int i = 0; i < workers; ++i) {
            Worker w = new Worker(this);
            (new Thread(w, "worker #"+i)).start();
            threads.addElement(w);
        }

		try {
			p("creating serversocket at port " + port);
			ss = new ServerSocket(port);
			while (isRunning) {
				
				Socket s = ss.accept();
				System.out.println("socket accepted");
				
				Worker w = null;
				synchronized (threads) {
					if (threads.isEmpty()) {
						Worker ws = new Worker(this);
						ws.setSocket(s);
						(new Thread(ws, "additional worker")).start();
					} else {
						w = (Worker) threads.elementAt(0);
						threads.removeElementAt(0);
						w.setSocket(s);
					}
				}
			}
			ss.close();
			System.out.println("stopping");
		} catch (IOException e) {
			System.err.println("Force stop");
			System.err.println(e.getMessage());
		}
    }

	@Override
	public synchronized ChatSessionController startNewChatSession(String name, String ipPort) {
		try {
			String tableFileName = String.format(USERS_TABLE_FILE, 
					ApplicationProperties.getInstance().getProperty(PROP_USERNAME));
			HashMap<String, String> usersTable = JsonParser.readFile(tableFileName);
			if (!usersTable.containsKey(name)) {
				usersTable.put(name, ipPort);
				JsonParser.writeFile(tableFileName, usersTable);
			}
		} catch (IOException e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
		}
		ChatSessionController newSession;
		synchronized (sessionLock) {
			newSession = chatFrame.addNewChatSession(new ChatSession(name, ipPort));
			chatSessions.add(newSession);
		}
		return newSession;
	}
	
	@Override
	public synchronized ChatSessionController startNewChatSession(ArrayList<ChatMate> chatmates) {
		try {
			String tableFileName = String.format(USERS_TABLE_FILE, 
					ApplicationProperties.getInstance().getProperty(PROP_USERNAME));
			HashMap<String, String> usersTable = JsonParser.readFile(tableFileName);
			for (ChatMate cm : chatmates)	
					usersTable.put(cm.getName(), cm.getIpPort());
			JsonParser.writeFile(tableFileName, usersTable);
		} catch (IOException e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
		}
		ChatSessionController newSession;
		synchronized (sessionLock) {
			newSession = chatFrame.addNewChatSession(new ChatSession(chatmates));
			chatSessions.add(newSession);
		}
		return newSession;
	}

	@Override
	public String searchChatmate(LoadingFrame loadingFrame, String name) throws Exception {
		loadingFrame.switchLoadingText("Searching");
		try {
			sendFindMateRequest(name);
			addDummyMessage = new TimerTask() {
				
				@Override
				public void run() {
					InputBuffer.getInstance().addMessage(new HttpMessage());
					
				}
			};
			responseWaitTimer.schedule(addDummyMessage, TIMERTASK_TIMEOUT);
			System.out.println("Getting message from buffer");
			HttpMessage msg = InputBuffer.getInstance().getMessage();
			System.out.println("Got message back");
			if (msg.getType() == RequestMethods.GET
					&& msg.getPath().equalsIgnoreCase(METHOD_SENDNAME)
					&& msg.getGETParameters().containsValue(name)) {
				HashMap<String, String> users = JsonParser.readFile(String.format(USERS_TABLE_FILE, 
							ApplicationProperties.getInstance().getProperty(PROP_USERNAME)));
				users.put(name, msg.getGETParameters().get(HEADER_KEY_IP));
				JsonParser.writeFile(String.format(USERS_TABLE_FILE, 
							ApplicationProperties.getInstance().getProperty(PROP_USERNAME)), users);
				return msg.getGETParameters().get(HEADER_KEY_IP);
			}		
		} catch (IOException e) {
			e.printStackTrace();
			throw new Exception("Failed to send search request");
		}
		return "";
	}

	@Override
	public void stopServer() {
		isRunning = false;
		
		System.out.println("Shutting the server down");
		for (Object w : threads) {
			((Worker) w).setSocketToNull();
		}
		try {
			ss.close();
		} catch (IOException e) {
			// Do nothing
		}
	}

	@Override
	public void restartServer() {
		isRunning = false;
		System.out.println("Restarting server");
		try {
			for (Object w : threads) {
				((Worker) w).setSocketToNull();
			}
			ss.close();
		} catch (IOException e) {
			// Do nothing
		}
		new Thread(this).start();
		
	}

	@Override
	public void startServer() {
		new Thread(this).start();
	}
	

	@Override
	public void searchChatmates(LoadingFrame loadingFrame) throws Exception {

		HashMap<String, String> users;
		try {
			users = JsonParser.readFile(String.format(USERS_TABLE_FILE, 
					ApplicationProperties.getInstance().getProperty(PROP_USERNAME)));
			HashMap<String, String> temp;
			BufferedReader br;
			URLConnection connection;
			String str;
			String line = "";
			HashMap<String, String> buffer = new HashMap<String, String>();
			for (String s1 : users.keySet()) {
				loadingFrame.switchLoadingText("Searching");
				try {
					String url = HEADER_HTTP_PREFIX
					+  users.get(s1) + METHOD_ASKNAMES
					+ QUESTION_MARK + HEADER_KEY_TTL + EQUALS + TIME_TO_LIVE;
					System.out.println(url);
					connection = (new URL(url)).openConnection();
					connection.setConnectTimeout(TIMEOUT_IN_MILLIS * 3);
					br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
				} catch (IOException se) {
					System.err.println("Client " + users.get(s1) + " unreachable");
					continue;
				}
				while ((str = br.readLine()) != null) line += str;
				br.close();
				loadingFrame.switchLoadingText("Reading data");
				temp = JsonParser.readString(line);
				for (String s2 : temp.keySet()) {
					if (!users.containsKey(s2) && !s2.equals(ApplicationProperties.getInstance().getProperty(PROP_USERNAME))) {
						buffer.put(s2, temp.get(s2));
					}
				}
			}
			for (String key : buffer.keySet()) {
				users.put(key, buffer.get(key));
			}
			JsonParser.writeFile(String.format(USERS_TABLE_FILE, 
					ApplicationProperties.getInstance().getProperty(PROP_USERNAME)), users);
		} catch (IOException e) {
			throw new Exception("Failed to send search request");
		}
		
	}

	@Override
	public boolean checkName(LoadingFrame loadingFrame, String name) throws Exception {
		loadingFrame.switchLoadingText("Checking availability");
		try {
			sendFindMateRequest(name);
			addDummyMessage = new TimerTask() {
				
				@Override
				public void run() {
					InputBuffer.getInstance().addMessage(new HttpMessage());
					
				}
			};
			responseWaitTimer.schedule(addDummyMessage, TIMERTASK_TIMEOUT);
			HttpMessage msg = InputBuffer.getInstance().getMessage();
			System.out.println("before if");
			if (msg.getType() == RequestMethods.GET
					&& msg.getPath().equalsIgnoreCase(METHOD_SENDNAME)
					&& msg.getGETParameters().containsValue(name)) {
				System.out.println("Name taken");
				return false;
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw new Exception("Failed to send search request");
		}
		System.out.println("Name free");
		return true;
	}
	
	private void sendFindMateRequest(String name) throws IOException {
		HashMap<String, String> users;
		users = JsonParser.readFile(String.format(USERS_TABLE_FILE, 
				ApplicationProperties.getInstance().getProperty(PROP_USERNAME)));
		InputBuffer.getInstance().setWritable();
		for (String s : users.keySet()) {
			String url = HEADER_HTTP_PREFIX +  users.get(s)
					+ METHOD_FINDNAME + QUESTION_MARK + HEADER_KEY_NAME + EQUALS + name
					+ AMPERSAND + HEADER_KEY_MYIP + EQUALS + ChatUtils.getMyIpPort()
					+ AMPERSAND + HEADER_KEY_TTL + EQUALS + TIME_TO_LIVE;
			System.out.println("Sending request " + url);
			try {
//				(new URL(url)).openStream();
				ChatUtils.makeGETRequestPrintResponse(url);
			} catch (IOException e){
				continue;
			}
		}
	}

	@Override
	public boolean isServerRunning() {
		return isRunning;
	}

	@Override
	public void killChatSession(ChatSessionController chatSession) {
		synchronized (sessionLock) {
			chatSessions.remove(chatSession);
		}
	}

	@Override
	public ArrayList<ChatSessionController> getChatSessions() {
		synchronized (sessionLock) {
			return chatSessions;
		}
	}
}